using System;
using System.Collections.Generic;
using System.Text;

namespace SsLib
{
    /// <summary>
    /// The TextObj class represents a text frame container in Sim#. Non-constant text frames are mutable.
    /// It encloses a character array MAIN of size SIZE.
    /// The flag CONST, if true, signals a constant text, which is immutable. 
    /// </summary>
    /// <remarks>
    /// A TextObj instance defines memory device that can contain text frames. A text frame that spans the entire length
    /// of the TextObj character array defines the Main text frame of the text frame container.
    /// 
    /// The TextObj is implemented as a class which makes the text frame container live on the garbage colleted heap.
    /// It also this makes the text frame container be passed as reference and shared between one or more Text variables.
    /// 
    /// Any text value processed by the program is contained within a text frame, 
    /// i.e. a non-empty segment of the MAIN array of some TEXTOBJ instance, or it is empty (i.e. notext).
    /// 
    /// A text variable is conceptually an instance of a composite structure Text.
    /// A Text structure contains a reference to a TextObj object, and the variables START, LENGTH, which indexes into
    /// the TextObj container and defines a text frame;  and POS, which indexes into the defined text frame. 
    /// 
    /// It comes that text frames might overlap if they are defined in the same TextObj reference.
    /// </remarks>
    /// <value></value>
    internal class TextObj
    {
        /// <summary>The internal character array of a text frame.</summary>
        internal readonly char[] MAIN;
        /// <summary>Indicates if the text frame is a constant.</summary>
        internal readonly bool CONST;
        /// <summary>The size of the text frame array.</summary>
        internal readonly int SIZE;

        public TextObj(int SIZE, bool CONST)
        {
            MAIN = new char[SIZE];
            this.CONST = CONST;
            this.SIZE = SIZE;
        }
    }

    /// <summary>
    /// The Text type represents a text value in Sim#. A text value is mutable.
    /// A Text variable references (and has as its value the contents of) some text frame defined by ist three first components 
    /// (a text frame container TextObj, and values START and LENGTH which indexes into the text frame container).
    /// POS identifies the current character. 
    /// </summary>
    /// <remarks>
    /// A text value is the same as the contents of a text frame.
    ///  
    /// The Text type is used much in the same way as String in .Net, but Text is mutable and a value type. 
    /// String is immutable and a reference type. Note: A constant text frame in Sim# is immutable and conseptually the same as a String in .Net.
    /// 
    /// The Text type in Sim# is implemented as a struct (value type). This makes the Text variables NOT live on the garbage collected heap.
    /// Text variables live on the eval stack, which makes much less impact on the garbage collector, and makes text variables
    /// very light weight in .Net. 
    /// 
    /// Every Text assignment using the '=' (assignment) operator in .Net makes a copy of the Text struct,
    /// not a reference. Changing the OBJ (TextObj) START, LENGTH and POS attributes of a Text variable does not alter or change the orginal copy.
    /// But chaning values inside the text frame (TextObj) instance, makes the change visible in other Text variables that references
    /// the same text frame container (TextObj instance).
    /// 
    /// Simula differentiates between value assignments ':=' operator, and reference assignmetns ':-' operator.
    /// The Text type are the only type in Simula that supportss both kinds of assignments, which makes Text variables a little elusive.
    /// 
    /// Text Reference assignmetns ':-' are the easiest to understand since its semantis are the same as the '=' operator in .Net. 
    /// It copies the OBJ, START, LENGTH and POS values from one Text variable to the other. Both text variables now references the same text frame
    /// (inside the same TextObj instance).
    /// 
    /// Text Value assignmetns ':=', however, copies the contents from the text frame on the right side to the text frame on the left side.
    /// If the left side text frame is larger then the text frame contentes copied, the remaing text frame characters is blanked. 
    /// 
    /// The characters of a text are accessible one at a time. Any text variable contains a "position indicator" POS, 
    /// which identifies the currently accessible character, if any, of the referenced text frame. 
    /// The position indicator of a given text variable X is an integer in the range (1,X.length+1). 
    ///
    /// The position indicator of a given text variable may be altered by the procedures "setpos", "getchar", and "putchar" 
    /// of the text variable. Also any of the text editing procedures may alter the position indicator of the text variable
    /// of which the procedure is an attribute. 
    ///
    /// Position indicators are ignored and left unaltered by text reference relations, text value relations and text value assignments. 
    /// </remarks>
    public struct Text
    {
        /// <summary>
        /// The special text reference NoText identifies "no frame". The value of NoText is the empty text value (String.Empty).
        /// </summary>
        public static Text NoText;

        /// <summary>
        /// Class constructor of the type Text. 
        /// Its only responsibility is to set the default values for the static field NoText.
        /// </summary>
        static Text()
        {
            NoText.OBJ = null;
            NoText.START = 1;
            NoText.LENGTH = 0;
            NoText.POS = 1;
        }

        /// <summary>References a TextObj instance (a text frame container)</summary>
        internal TextObj OBJ;
        /// <summary>Defines the start position of the text frame within the TextObj instance.</summary>
        internal int START;
        /// <summary>Defines the length of the text frame within the TextObj instance.</summary>
        internal int LENGTH;
        /// <summary>Indentifies the current character position within the text frame.</summary>
        internal int POS;

        #region Access to attributes of the text frame.
        /// <summary>
        /// Returns true if the text value is a constant.
        /// </summary>
        /// <returns>True if text value is a constant, otherwise false.</returns>
        public bool Constant()
        {
            if (OBJ == null) return true;
            else return OBJ.CONST;
        }
        /// <summary>
        /// Returns the start position of this text value within its Main text frame.
        /// </summary>
        /// <returns>The start position of this text value within its Main text frame.</returns>
        public int Start()
        {
            return START;
        }
        /// <summary>
        /// Returns the length of this text value.
        /// </summary>
        /// <returns>The length of this text value.</returns>
        public int Length()
        {
            return LENGTH;
        }
        /// <summary>
        /// Returns the Main text frame from which this text value is a sub text text frame.
        /// I.e. a text value of the entire character array of the text frame container.
        /// </summary>
        /// <remarks>
        /// A frame which is completely contained by another frame is called a "subframe" of that frame. 
        /// The text frame associated with the entire array attribute MAIN is called the "main frame" 
        /// of the text object. 
        /// All frames of the text object (TextObj instance) are subframes of the main frame. 
        /// </remarks>
        /// <returns>The text value of the Main frame that contains the text frame of </returns>
        public Text Main()
        {
            if (OBJ == null) return NoText;
            Text T;
            T.OBJ = OBJ;
            T.START = 1;
            T.LENGTH = OBJ.SIZE;
            T.POS = 1;
            return T;
        }
        #endregion

        #region Character access
        /// <summary>
        /// Returns the current character position whithin the text frame.
        /// </summary>
        /// <returns>The current character position whithin the text frame.</returns>
        public int Pos()
        {
            return POS;
        }
        /// <summary>
        /// Alters the current position indicator within the text frame.
        /// If the requested position is outside the text frame boudaries then the position is set to Length + 1;
        /// </summary>
        /// <param name="i">The new current position within the text frame.</param>
        public void SetPos(int i)
        {
            if ((i < 1) || (i > LENGTH + 1)) POS = LENGTH + 1;
            else POS = i;
        }
        /// <summary>
        /// Returns true if the current position is less then or equal to the text frame length.
        /// </summary>
        /// <returns>True if there is more characters in the text frame, relative to the current position.</returns>
        public bool More()
        {
            return POS <= LENGTH;
        }
        /// <summary>
        /// Returns the character at the current position and advances the current position by 1.
        /// </summary>
        /// <returns>The character at the current position.</returns>
        public char GetChar()
        {
            if (POS > LENGTH)
            {
                Environment.Error("GetChar: POS out of range");
                return char.MinValue;
            }
            else
            {
                return OBJ.MAIN[START + POS++ - 2];
            }
        }
        /// <summary>
        /// Alters the character at the current position within the text frame and advances the current position by 1.
        /// </summary>
        /// <param name="c">The new character to assign to the current position.</param>
        public void PutChar(char c)
        {
            if ((OBJ == null) || (OBJ.CONST) || (POS > LENGTH))
            {
                Environment.Error("PutChar: Illegal operation");
            }
            else
            {
                OBJ.MAIN[START + POS++ - 2] = c;
            }
        }
        #endregion

        #region Subtexts
        /// <summary>
        /// Returns a subframe from this text frame, 'n' characters long, starting at position 'i'.
        /// </summary>
        /// <remarks>
        /// If legal, "X.sub(i,n)" references that subframe of X whose first
        /// character is character number i of X, and which contains n
        /// consecutive characters. The POS attribute of the expression
        /// defines a local numbering of the characters within the subframe.
        /// If n = 0, the expression references notext.
        ///
        /// If legal, the following Boolean expressions are true for any text
        /// variable X:
        /// <code>
        ///     X.sub(i,n).sub(j,m) == X.sub(i+j-1,m)
        ///
        ///     n <> 0  imp  X.main == X.sub(i,n).main
        ///
        ///     X.main.sub(X.start,X.length) == X
        /// </code>
        /// </remarks>
        /// <param name="i">Starting position of the subframe.</param>
        /// <param name="n">Length of the subframe.</param>
        /// <returns>A subframe from this text frame.</returns>
        public Text Sub(int i, int n)
        {
            if ((i < 0) || (n < 0) || (i - 1 + n > Length())) Environment.Error("Sub: out of range");
            if (n == 0) return Text.NoText;

            Text T;
            T.OBJ = OBJ;
            T.START = START + i - 1;
            T.LENGTH = n;
            T.POS = 1;
            return T;
        }
        /// <summary>
        /// Returns a subframe og this text frame stripping any trailing blanks.
        /// </summary>
        /// The expression "X.strip" is equivalent to "X.sub(1,n)", where n
        /// indicates the position of the last non-blank character in X. If X
        /// does not contain any non-blank character, notext is returned.
        ///
        /// Let X and Y be text variables. Then after the value assignment
        /// "X:=Y", if legal, the relation "X.strip = Y.strip" has the value
        /// true, while "X = Y" is true only if X.length = Y.length.
        /// <returns>A subframe og this text value without trailing blanks.</returns>
        public Text Strip()
        {
            int i = this.LENGTH;
            if (i == 0) return Text.NoText;
            while (this.OBJ.MAIN[i - 1] == ' ') i--;
            if (i == 0) return Text.NoText;
            return this.Sub(1, i);
        }
        #endregion

        #region De-editing procedures
        /// <summary>
        /// Skips any leading blanks or tabs
        /// </summary>
        private void SkipLeadingBlanks()
        {
            char ch = GetChar();
            while (More() && (ch == ' ' || ch == '\t')) ch = GetChar();
            this.POS--;
        }
        /// <summary>
        /// Converts the string representation of a number in the text stream to an integer,
        /// starting from current possition. 
        /// </summary>
        /// <remarks>
        /// The procedure locates an INTEGER ITEM.  The function value is equal to the corresponding integer.
        /// </remarks>
        /// <returns>An integer value converted from the text frame.</returns>
        public int GetInt()
        {
            StringBuilder sb = new StringBuilder();

            //skip leading spaces or tabs
            SkipLeadingBlanks();

            if (More())
            {
                char c = this.GetChar();
                if (c == '-' || c == '+')
                {
                    sb.Append(c);
                    SkipLeadingBlanks();
                }
                else POS--;
                while (More())
                {
                    c = this.GetChar();
                    if (c >= '0' && c <= '9') sb.Append(c);
                    else break;
                }
                this.POS--;
            }
            string s = sb.ToString();
            return int.Parse(s);
        }
        /// <summary>
        /// Converts the string representation of a number in the text stream to a long real (Double),
        /// starting from current possition. 
        /// </summary>
        /// <remarks>
        /// The procedure locates a REAL ITEM.  The function value is equal
        /// to or approximates to the corresponding number. An INTEGER ITEM
        /// exceeding a certain implementation-defined range may lose
        /// precision when converted to long real.
        ///
        /// Note: No distinction is made between real and long real items.
        ///   In order to preserve precision the procedure assumes long real
        ///   precision.
        /// </remarks>
        /// <returns>A long real value converted from the text frame.</returns>
        public double GetReal()
        {
            StringBuilder sb = new StringBuilder();

            //skip leading spaces
            SkipLeadingBlanks();

            while (this.More())
            {
                char c = this.GetChar();
                if (c >= '0' && c <= '9' || c == ',') sb.Append(c);
                else break;

            }
            this.POS--;
            string s = sb.ToString();
            return double.Parse(s);
        }
        /// <summary>
        /// Converts the string representation of a grouped number item in the text stream to an integer,
        /// starting from current possition. 
        /// </summary>
        /// <remarks>
        /// The procedure locates a GROUPED ITEM.  The function value is
        /// equal to the resulting integer. The digits of a GROUPED ITEM
        /// may be interspersed with BLANKS and a single DECIMAL MARK which
        /// are ignored by the procedure.
        /// </remarks>
        /// <returns>An integer value converted from the text frame.</returns>
        public int GetFrac()
        {
            StringBuilder sb = new StringBuilder();

            //skip leading spaces
            SkipLeadingBlanks();

            while (this.More())
            {
                char c = this.GetChar();

                if (char.IsPunctuation(c) || char.IsWhiteSpace(c) || char.IsSeparator(c)) continue;
                if (char.IsDigit(c) || c == '+' || c == '-') sb.Append(c);
                else break;

            }
            this.POS--;
            string s = sb.ToString();
            return int.Parse(s);
        }
        #endregion

        #region Editing procedures
        /// <summary>
        /// Converts an integer value to a string and writes it into the text frame at the current position.
        /// </summary>
        /// <remarks>
        /// The value of the parameter is converted to an INTEGER ITEM which
        /// designates an integer equal to that value.
        /// </remarks>
        /// <param name="i">The integer to convert and write into the text frame.</param>
        public void PutInt(int i)
        {
            int si, di, c; //source index, dest index and count
            string s = i.ToString();

            int room = this.LENGTH - this.POS + 1;

            if (s.Length > room)
            {
                //si = s.Length - room;
                //di = this.POS;
                //c = room;
                //bugfix: according to Simula-86,ch.8 Editing Procedures
                //the frame must be filled with astrisks if frame is too small to fit the number.
                while (this.POS <= this.LENGTH) this.PutChar('*');
            }
            else
            {
                si = 0;
                di = this.POS + (room - s.Length) - 1;
                c = s.Length;

                //bugfix: forgot to add start-1. This caused the put... procs to
                //overwrite from beginning of text frame array.
                Array.Copy(s.ToCharArray(), si, this.OBJ.MAIN, di + START - 1, c);
                this.POS = this.LENGTH + 1;                                        
            }                                                                      
        }
        /// <summary>
        /// Converts an long real (Double) value to a decimal string and writes it into the text frame at the current position.
        /// </summary>
        /// <remarks>
        /// The resulting numeric item is an INTEGER ITEM if n=0 or a DECIMAL
        /// ITEM with a FRACTION of n digits if n>0. It designates a number
        /// equal to the value of r or an approximation to the value of r,
        /// correctly rounded to n decimal places. If n&lt;0, a run-time error
        /// is caused.
        /// </remarks>
        /// <param name="r">The long real value to convert and write into the text frame.</param>
        /// <param name="n">The number of fraction digits of the decimal to write into the text frame.</param>
        public void PutFix(double r, int n)
        {
            int si, di, c; //source index, dest index and count
            string format = "{0:F" + n + "}";
            string s = string.Format(format, r);

            int room = this.LENGTH - this.POS + 1;

            if (s.Length > room)
            {
                si = s.Length - room;
                di = this.POS;
                c = room;
            }
            else
            {
                si = 0;
                di = this.POS + (room - s.Length) - 1;
                c = s.Length;
            }
            Array.Copy(s.ToCharArray(), si, this.OBJ.MAIN, di + START - 1, c);
            this.POS = this.LENGTH + 1;
        }
        /// <summary>
        /// Converts an long real (Double) value to an exponent string and writes it into the text frame at the current position.
        /// </summary>
        /// <remarks>
        /// The resulting numeric item is a REAL ITEM containing an EXPONENT
        /// with a fixed implementation-defined number of characters. The
        /// EXPONENT is preceded by a SIGN PART if n=0, or by an INTEGER ITEM
        /// with one digit if n=1, or if n>1, by a DECIMAL ITEM with an
        /// INTEGER ITEM of 1 digit only, and a fraction of n-1 digits. If
        /// n&lt;0 a runtime error is caused.
        /// </remarks>
        /// <param name="r">The long real value to convert and write into the text frame.</param>
        /// <param name="n">The number of the exponent part digits of the real number to write into the text frame.</param>
        public void PutReal(double r, int n)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// Converts an integer into a grouped decimal string and writes it into the text frame at the current position.
        /// </summary>
        /// <remarks>
        /// The resulting numeric item is a GROUPED ITEM with no DECIMAL MARK
        /// if n&lt;=0, and with a DECIMAL MARK followed by total of n digits if
        /// n>0. Each digit group consists of 3 digits, except possibly the
        /// first one, and possibly the last one following a DECIMAL MARK. The
        ///S numeric item is an exact representation of the number i * 10**(-n).
        /// </remarks>
        /// <param name="i">The integer to convert into a grouped decimal string.</param>
        /// <param name="n">The number of fraction digits of the converted grouped decimal string.</param>
        public void PutFrac(int i, int n)
        {
            int si, di, c; //source index, dest index and count

            double d = (double)i / Math.Pow(10,n);
            string s = d.ToString("### ### ### ### ###.### ### ### ###");
            s = s.Trim();
            int room = this.LENGTH - this.POS + 1;

            if (s.Length > room)
            {
                si = s.Length - room;
                di = this.POS;
                c = room;
            }
            else
            {
                si = 0;
                di = this.POS + (room - s.Length) - 1;
                c = s.Length;
            }
            Array.Copy(s.ToCharArray(), si, this.OBJ.MAIN, di + START - 1, c);
            this.POS = this.LENGTH + 1;
        }
        #endregion

        #region Supporting methods and operators to make Text type more well behaved in the .Net Framework
        /// <summary>
        /// Performes a text value assignement into this text frame.
        /// </summary>
        /// <param name="t">The text value to value assign into the this text frame.</param>
        public void SetValue(Text t)
        {
            if (this.Equals(NoText))
            {
                if (t.Equals(NoText)) ;
                else Environment.Error("SetValue: cannot value assign a text to notext");
            }
            if ((this.OBJ.CONST) || (this.LENGTH < t.LENGTH))
                Environment.Error("SetValue: text is constant or too small to value assign");

            //Copy the text values from t to this text frame.
            if (t.LENGTH > 0)
            Array.Copy(t.OBJ.MAIN, t.START - 1, this.OBJ.MAIN, 0, t.LENGTH);

            //Blank remaining part of this text frame.
            for (int i = this.START - 1 + t.LENGTH; i < this.START + this.LENGTH - 1; i++)
                this.OBJ.MAIN[i] = ' ';
        }
        /// <summary>
        /// CompareTo method compares a given text value with this text frame.
        /// </summary>
        /// <param name="t">The text value to compare with this text frame.</param>
        /// <returns>0 if the text values are equal, otherwise non-zero.</returns>
        public int CompareTo(Text t)
        {
            return Text.Compare(this, t);
        }
        /// <summary>
        /// Converts this text value into a String object.
        /// </summary>
        /// <returns>A String containing the value of this text frame.</returns>
        public override string ToString()
        {
            if (this.Equals(NoText)) return string.Empty;
            else
            {
                StringBuilder sb = new StringBuilder();
                sb.Append(Environment._Copy(this).OBJ.MAIN);
                return sb.ToString();
            }
        }
        /// <summary>
        /// Text concatenation method. Concatenates two text values and returns the concatenated text in a new text frame.
        /// </summary>
        /// <remarks>
        /// Forwards the concatenation operation to the + operator.
        /// Consider to remove this static method as it is not needed anymore.
        /// </remarks>
        /// <param name="t1">A text value to convatenate.</param>
        /// <param name="t2">A text value to convatenate.</param>
        /// <returns>The convatenated text values in a new text frame.</returns>
        public static Text ConCat(Text t1, Text t2)
        {
            return t1 + t2;
        }
        /// <summary>
        /// Text concatenation method. Concatenates two text values and returns the concatenated text in a new text frame.
        /// </summary>
        /// <param name="t1">A text value to convatenate.</param>
        /// <param name="t2">A text value to convatenate.</param>
        /// <returns>The convatenated text values in a new text frame.</returns>
        public static Text operator +(Text t1, Text t2)
        {
            Text newT = new Text();

            newT.OBJ = new TextObj(t1.LENGTH + t2.LENGTH, false);
            newT.START = 1;
            newT.POS = 1;
            newT.LENGTH = t1.LENGTH + t2.LENGTH;
            t1.OBJ.MAIN.CopyTo(newT.OBJ.MAIN, 0);
            t2.OBJ.MAIN.CopyTo(newT.OBJ.MAIN, t1.LENGTH);
            return newT;
        }
        /// <summary>
        /// Converts a String into a constant text value in new text frame.
        /// </summary>
        /// <param name="s">The string to convert into a text value.</param>
        /// <returns>A constant text value converted from a String.</returns>
        public static implicit operator Text(string s)
        {
            if (s == string.Empty) return Text.NoText;
            else
            {
                Text T;
                T.OBJ = new TextObj(s.Length, true);    
                T.POS = 1;
                T.START = 1;
                T.LENGTH = s.Length;
                s.ToCharArray().CopyTo(T.OBJ.MAIN, 0);
                return T;
            }
        }
        /// <summary>
        /// Compare method compares two text values, returning an integer value with the result.
        /// </summary>
        /// <param name="t1">The first text value.</param>
        /// <param name="21">The second text value.</param>
        /// <returns>0 if the text values are equal, otherwise non-zero.</returns>
        public static int Compare(Text t1, Text t2)
        {
            string s1 = t1.ToString();
            string s2 = t2.ToString();

            return string.Compare(s1, s2);
        }
        #endregion
    }
}
